// System.IO.BufferedStream
// Updated to work with .NET Micro Framework
// Changes by:
//     Chris Taylor (taylorza) - 26/08/2012

// System.IO.BufferedStream
//
// Author:
//   Matt Kimball (matt@kimball.net)
//   Ville Palo <vi64pa@kolumbus.fi>
//
// Copyright (C) 2004 Novell (http://www.novell.com)
//

//
// Copyright (C) 2004 Novell, Inc (http://www.novell.com)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.IO;

namespace dotnetwarrior.NetMF.IO
{
  public sealed class BufferedStream : Stream
  {
    Stream m_stream;
    byte[] m_buffer;
    int m_buffer_pos;
    int m_buffer_read_ahead;
    bool m_buffer_reading;
    private bool disposed = false;

    public BufferedStream(Stream stream)
      : this(stream, 4096)
    {
    }

    public BufferedStream(Stream stream, int buffer_size)
    {
      if (stream == null)
        throw new ArgumentNullException("stream");
      // LAMESPEC: documented as < 0
      if (buffer_size <= 0)
        throw new ArgumentOutOfRangeException("buffer_size", "<= 0");
      if (!stream.CanRead && !stream.CanWrite)
      {
        throw new ObjectDisposedException("Cannot access a closed Stream.");
      }

      m_stream = stream;
      m_buffer = new byte[buffer_size];
    }

    public override bool CanRead
    {
      get
      {
        return m_stream.CanRead;
      }
    }

    public override bool CanWrite
    {
      get
      {
        return m_stream.CanWrite;
      }
    }

    public override bool CanSeek
    {
      get
      {
        return m_stream.CanSeek;
      }
    }

    public override long Length
    {
      get
      {
        Flush();
        return m_stream.Length;
      }
    }

    public override long Position
    {
      get
      {
        CheckObjectDisposedException();
        return m_stream.Position - m_buffer_read_ahead + m_buffer_pos;
      }

      set
      {
        if (value < Position && (Position - value <= m_buffer_pos) && m_buffer_reading)
        {
          m_buffer_pos -= (int)(Position - value);
        }
        else if (value > Position && (value - Position < m_buffer_read_ahead - m_buffer_pos) && m_buffer_reading)
        {
          m_buffer_pos += (int)(value - Position);
        }
        else
        {
          Flush();
          m_stream.Position = value;
        }
      }
    }

    public override void Close()
    {
      try
      {
        if (m_buffer != null)
          Flush();
      }
      finally
      {
        m_stream.Close();
        m_buffer = null;
        disposed = true;
      }
    }

    public override void Flush()
    {
      CheckObjectDisposedException();

      if (m_buffer_reading)
      {
        if (CanSeek)
          m_stream.Position = Position;
      }
      else if (m_buffer_pos > 0)
      {
        m_stream.Write(m_buffer, 0, m_buffer_pos);
      }

      m_buffer_read_ahead = 0;
      m_buffer_pos = 0;
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
      CheckObjectDisposedException();
      if (!CanSeek)
      {
        throw new NotSupportedException("Non seekable stream.");
      }
      Flush();
      return m_stream.Seek(offset, origin);
    }

    public override void SetLength(long value)
    {
      CheckObjectDisposedException();

      if (value < 0)
        throw new ArgumentOutOfRangeException("value must be positive");

      if (!m_stream.CanWrite && !m_stream.CanSeek)
        throw new NotSupportedException("the stream cannot seek nor write.");

      if ((m_stream == null) || (!m_stream.CanRead && !m_stream.CanWrite))
        throw new IOException("the stream is not open");

      m_stream.SetLength(value);
      if (Position > value)
        Position = value;
    }

    public override int ReadByte()
    {
      CheckObjectDisposedException();

      byte[] b = new byte[1];

      if (Read(b, 0, 1) == 1)
      {
        return b[0];
      }
      else
      {
        return -1;
      }
    }

    public override void WriteByte(byte value)
    {
      CheckObjectDisposedException();
      byte[] b = new byte[1];

      b[0] = value;
      Write(b, 0, 1);
    }

    public override int Read(byte[] array, int offset, int count)
    {
      if (array == null)
        throw new ArgumentNullException("array");
      CheckObjectDisposedException();
      if (!m_stream.CanRead)
      {
        throw new NotSupportedException("Cannot read from stream");
      }
      if (offset < 0)
        throw new ArgumentOutOfRangeException("offset", "< 0");
      if (count < 0)
        throw new ArgumentOutOfRangeException("count", "< 0");
      // re-ordered to avoid possible integer overflow
      if (array.Length - offset < count)
        throw new ArgumentException("array.Length - offset < count");

      if (!m_buffer_reading)
      {
        Flush();
        m_buffer_reading = true;
      }

      if (count <= m_buffer_read_ahead - m_buffer_pos)
      {
        Array.Copy(m_buffer, m_buffer_pos, array, offset, count);

        m_buffer_pos += count;
        if (m_buffer_pos == m_buffer_read_ahead)
        {
          m_buffer_pos = 0;
          m_buffer_read_ahead = 0;
        }

        return count;
      }

      int ret = m_buffer_read_ahead - m_buffer_pos;
      Array.Copy(m_buffer, m_buffer_pos, array, offset, ret);
      m_buffer_pos = 0;
      m_buffer_read_ahead = 0;
      offset += ret;
      count -= ret;

      if (count >= m_buffer.Length)
      {
        ret += m_stream.Read(array, offset, count);
      }
      else
      {
        m_buffer_read_ahead = m_stream.Read(m_buffer, 0, m_buffer.Length);

        if (count < m_buffer_read_ahead)
        {
          Array.Copy(m_buffer, 0, array, offset, count);
          m_buffer_pos = count;
          ret += count;
        }
        else
        {
          Array.Copy(m_buffer, 0, array, offset, m_buffer_read_ahead);
          ret += m_buffer_read_ahead;
          m_buffer_read_ahead = 0;
        }
      }

      return ret;
    }

    public override void Write(byte[] array, int offset, int count)
    {
      if (array == null)
        throw new ArgumentNullException("array");
      CheckObjectDisposedException();
      if (!m_stream.CanWrite)
      {
        throw new NotSupportedException("Cannot write to stream");
      }
      if (offset < 0)
        throw new ArgumentOutOfRangeException("offset", "< 0");
      if (count < 0)
        throw new ArgumentOutOfRangeException("count", "< 0");
      // avoid possible integer overflow
      if (array.Length - offset < count)
        throw new ArgumentException("array.Length - offset < count");

      if (m_buffer_reading)
      {
        Flush();
        m_buffer_reading = false;
      }

      // reordered to avoid possible integer overflow
      if (m_buffer_pos >= m_buffer.Length - count)
      {
        Flush();
        m_stream.Write(array, offset, count);
      }
      else
      {
        Array.Copy(array, offset, m_buffer, m_buffer_pos, count);
        m_buffer_pos += count;
      }
    }

    private void CheckObjectDisposedException()
    {
      if (disposed)
      {
        throw new ObjectDisposedException("Stream is closed");
      }
    }
  }
}
